import { extractSchemas } from "extract-pg-schema";
import { rimraf } from "rimraf";

import type { Config, InstantiatedConfig, PreRenderHook } from "./config-types";

type DerivedExtensions = {
  fileExtension: ".ts" | ".mts" | ".cts";
  importsExtension: "" | ".js" | ".mjs" | ".cjs";
};

const deriveExtensions = (
  tsModuleFormat:
    | "esm"
    | "commonjs"
    | "explicit-esm"
    | "explicit-commonjs"
    | undefined,
  importsExtension: string | undefined,
): DerivedExtensions => {
  // If importsExtension is explicitly set, use legacy behavior
  if (importsExtension) {
    return {
      fileExtension: ".ts",
      importsExtension: importsExtension as "" | ".js" | ".mjs" | ".cjs",
    };
  }

  // Derive from tsModuleFormat
  switch (tsModuleFormat) {
    case "esm":
      return { fileExtension: ".ts", importsExtension: ".js" };
    case "commonjs":
      return { fileExtension: ".ts", importsExtension: "" };
    case "explicit-esm":
      return { fileExtension: ".mts", importsExtension: ".mjs" };
    case "explicit-commonjs":
      return { fileExtension: ".cts", importsExtension: ".cjs" };
    default:
      // Default to .ts with no extension for backwards compatibility
      return { fileExtension: ".ts", importsExtension: "" };
  }
};
import { runWithContext } from "./context";
import {
  defaultGenerateIdentifierType,
  defaultGetMetadata,
  defaultGetPropertyMetadata,
  defaultGetRoutineMetadata,
  defaultPropertySortFunction,
} from "./default-metadata-generators";
import defaultTypeMap from "./defaultTypeMap";
import makeCompositeGenerator from "./generators/makeCompositeGenerator";
import domainsGenerator from "./generators/domainsGenerator";
import enumsGenerator from "./generators/enumsGenerator";
import rangesGenerator from "./generators/rangesGenerator";
import makeRoutineGenerator from "./generators/makeRoutineGenerator";
import applyTaggedComments from "./hooks/applyTaggedComments";
import markAsGenerated from "./hooks/markAsGenerated";
import type Output from "./Output";
import renderTsFile from "./ts-utilities/renderTsFile";
import type TypeMap from "./TypeMap";
import writeFile from "./writeFile";

type Progress = {
  onProgressStart?: (total: number) => void;
  onProgress?: () => void;
  onProgressEnd?: () => void;
};

const defaultConfig: Partial<Config> = {
  getMetadata: defaultGetMetadata,
  getPropertyMetadata: defaultGetPropertyMetadata,
  generateIdentifierType: defaultGenerateIdentifierType,
  propertySortFunction: defaultPropertySortFunction,
  getRoutineMetadata: defaultGetRoutineMetadata,
  outputPath: ".",
  enumStyle: "enum",
  resolveViews: true,
  preDeleteOutputFolder: false,
};

const processDatabase = async (
  cfg: Config,
  progress?: Progress,
): Promise<void> => {
  const config = { ...defaultConfig, ...cfg };
  const schemas = await extractSchemas(config.connection, {
    schemas: config.schemas,
    typeFilter: config.typeFilter,
    ...progress,
  });

  const typeMap: TypeMap = {
    ...defaultTypeMap,
    ...config.customTypeMap,
  };

  if (config.tsModuleFormat && config.importsExtension) {
    throw new Error(
      "Cannot use both tsModuleFormat and importsExtension at the same time",
    );
  }

  const { fileExtension, importsExtension } = deriveExtensions(
    config.tsModuleFormat,
    config.importsExtension,
  );

  const instantiatedConfig: InstantiatedConfig = {
    getMetadata: config.getMetadata,
    getPropertyMetadata: config.getPropertyMetadata,
    generateIdentifierType: config.generateIdentifierType,
    propertySortFunction: config.propertySortFunction,
    getRoutineMetadata: config.getRoutineMetadata,
    enumStyle: config.enumStyle,
    typeMap,
    schemas,
    connection: config.connection,
    outputPath: config.outputPath,
    preDeleteOutputFolder: config.preDeleteOutputFolder,
    resolveViews: config.resolveViews,
    importsExtension: importsExtension || undefined,
    tsModuleFormat: config.tsModuleFormat,
    fileExtension,
  };

  await runWithContext({ instantiatedConfig }, async () => {
    const generators = [
      makeCompositeGenerator("table"),
      makeCompositeGenerator("foreignTable"),
      makeCompositeGenerator("view"),
      makeCompositeGenerator("materializedView"),
      makeCompositeGenerator("compositeType"),
      enumsGenerator,
      rangesGenerator,
      domainsGenerator,
      makeRoutineGenerator("function"),
      makeRoutineGenerator("procedure"),
    ];

    let output: Output = {};
    Object.values(schemas).forEach((schema) => {
      generators.forEach((generator) => {
        output = generator(schema, output);
      });
    });

    const preRenderHooks: PreRenderHook[] = [
      applyTaggedComments,
      ...(config.preRenderHooks ?? []),
    ];
    for (const hook of preRenderHooks) {
      output = await hook(output, instantiatedConfig);
    }

    let filesToWrite = Object.keys(output).map((path) => {
      const file = output[path];

      if (!file.fileType) {
        // Hack for backwards compatibility.
        file.fileType = "typescript";
      }

      if (file.fileType === "typescript") {
        const lines = renderTsFile(file.declarations, path);
        return {
          fullPath: `${path}${instantiatedConfig.fileExtension}`,
          lines,
        };
      } else if (file.fileType === "generic") {
        return { fullPath: path, lines: file.lines };
      }
      throw new Error(`Path ${path} is an unknown file type`);
    });

    const postRenderHooks = config.postRenderHooks ?? [markAsGenerated];
    for (const hook of postRenderHooks) {
      filesToWrite = await Promise.all(
        filesToWrite.map(async (file) => {
          const lines = await hook(
            file.fullPath,
            file.lines,
            instantiatedConfig,
          );
          return { ...file, lines };
        }),
      );
    }

    if (instantiatedConfig.preDeleteOutputFolder) {
      console.info(`Clearing old files in ${instantiatedConfig.outputPath}`);
      await rimraf(instantiatedConfig.outputPath, { glob: true });
    }

    filesToWrite.forEach((file) => writeFile(file));
  });
};

export default processDatabase;
